iT邦幫忙

2025 iThome 鐵人賽

DAY 7
0

哈囉~大家,恭喜來到第一週的最後一天 Day 7 ! 🙌 🙌 🙌
在前面幾天學完 Go 的基本語法練習之後,這個章節就要來介紹 Go 的單元測試!

什麼是單元測試呢?

我們以齒輪為例。每一個齒輪就像是一個單元,而齒輪跟齒輪之間又可以組裝成一個比較大的齒輪(大單元),以此類推… 最後完成一個機械設備。

而當我們要量產這些機械設備的時候,就會需要好多齒輪來組裝。但又不希望這些組裝完成的設備品質參差不齊,所以這時候單個齒輪的品質就很重要!

每個小齒輪製作完成後,需要經過一系列的檢驗,看看有沒有符合完美齒輪的標準。
👉 而這就是「 單元測試 」!

單元測試的概念就是測試程式最小功能 function。讓我們的程式能夠在越長越大的時候,依然保有一定的品質。
理解完大致概念之後,就要來進入 Go 的單元測試的介紹了~

 

Go 單元測試介紹

先來介紹一些命名的規則以及需要特別注意的地方:

  1. 測試檔命名規則
    • 測試檔要跟程式放在同一個 package 。
    • 檔名要以 _test.go 結尾。 (例如:calc.go → 測試檔命名為 calc_test.go
  2. 測試函式規則
    • 必須以 Test 開頭。
    • 傳入參數固定為 t *testing.T
    • 不能有回傳值。
    • t.Errort.Fatalf 方法印出錯誤訊息。
      • t.Errorf → 報錯但繼續跑其他測試。
      • t.Fatalf → 報錯並立刻中止測試。

基本測試

我們拿前面有提到的相加函式來舉例。以下是主要程式以及測試檔案內容:

// 要測試的程式 calc.go 

package calc

func Add(a, b int) int{
	return a + b
}
// 測試檔案 calc_test.go
 
package calc
import "testing"

func TestAdd(t *testing.T) {
	result := Add(2, 3)
	if result != 5 {
		t.Errorf("Add(2,3) 預期=5,得到=%d", result)  // 印出錯誤訊息
	}
}

執行時,要記得先到這兩個檔案的路徑底下輸入指令才能順利跑測試喔!
以下這兩個指令都可以~ 差別在於顯示的內容詳細程度。

// 輸入測試指令
go test
go test -v

分別輸出:

  1. go test :
    https://ithelp.ithome.com.tw/upload/images/20250921/20178223lgql44O1Ss.png

  2. go test -v :
    https://ithelp.ithome.com.tw/upload/images/20250921/20178223N3S8UdxpQw.png

那如果是測試失敗的話會長什麼樣子呢? 就會看到有印出 FAIL 的字樣!
測試失敗輸出:
https://ithelp.ithome.com.tw/upload/images/20250921/20178223W1s5N7u0Qe.png

這是最基礎的測試撰寫和操作方式~

再來我們來看看,可不可以新增多的測試資料? 答案是:當然可以!
這時候就要結合我們前面學到的 array 還有 struct 的功能啦~


多個測試和子測試

這邊除了介紹測試多個測資,我們再多放入子測試的概念,畢竟這兩個很常會一起使用~所以我們就一起介紹!

什麼叫做 子測試(Subtests)? 就是在一個測試 function 裡面還有一個測試,那裡面的測試就叫做子測試。有點像一個傳入一個的概念,我們把主要的測試內容寫在一開始的測試 function,執行測試時,這些測試資料就會傳入子測試!

會有子測試的原因就是,當你需要記錄測試名稱時,你可以使用這樣的方式~

子測試的建立方式:

  • 使用 t.Run() 方式。
  • name:測試資料名稱。
  • f:子測試的 function,接收一開始定義好的測資。
// 子測試建立方式
t.Run(name string, f func(t *testing.T))

來看看範例:

// 新增多個測資的測試程式 calc_test.go 
package calc
import "testing"

func TestAdd(t *testing.T) {
	tests := []struct {
		name string                   // 新增測試名稱,不新增也是可以的
		a, b int
		sum int
	}{
		{"test 1", 3, 2, 5},          // 測試資料
		{"test 2", 5, 0, 5},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {       // 這邊是子測試(Subtests)
			got := Add(tt.a, tt.b)
			if got != tt.sum {
				t.Errorf("got %d, sum %d", got, tt.sum)
			}
		})
	}
}

輸出:
測試名稱也會同時印出來,方便我們 debug 用~
https://ithelp.ithome.com.tw/upload/images/20250921/20178223oEcBBuQlsV.png

有單個測試、多個測試當然也有平行測試!

什麼叫做「平行測試」呢?
我們都知道程式執行時,是一行一行接著跑,而平行測試就是可以同時執行的意思!言下之意,表示可以加速整個測試的過程,減少測試時間。

但它只能使用在「互相不影響的測試程式中」!!!


平行測試

Go 提供 t.Parallel() 方法,讓測試程式可以同時執行。延續上面的程式,要怎麼加入平行測試呢?

範例:

// 新增平行測試的測試程式 calc_test.go 
package calc
import (
	"testing"
	"time"
)

func TestAddParallel(t *testing.T) {
	tests := []struct {
		name string
		a, b int
		sum  int
	}{
		{"test 1", 3, 2, 5},         
		{"test 2", 5, 0, 5},
	}

	for _, tt := range tests {
		tt := tt                          // 避免抓到同一個變數!!
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()                  // 讓子測試平行執行,也就是 test 1, 2 會一起執行
			time.Sleep(1 * time.Second)   // 增加一些運算時間,避免看不出差異

			got := Add(tt.a, tt.b)
			if got != tt.sum {
				t.Errorf("got %d, want %d", got, tt.sum)
			}
		})
	}
}

輸出:(有使用 t.Parallel()
https://ithelp.ithome.com.tw/upload/images/20250921/20178223IPzGrw3pbb.png

那我們來看看,如果沒有使用 t.Parallel() ,會需要多少時間?
先把這兩行程式註解,然後再跑一次測試~

// 註解以下程式
// tt := tt
// t.Parallel() 

輸出(沒有使用 t.Parallel()):
https://ithelp.ithome.com.tw/upload/images/20250921/20178223CA6Gw6aXBW.png

結論是,沒有使用平行測試需要大概 3 秒;使用平行測試則只要 2 秒。雖然這個範例減少的時間沒有很有感覺 😆 但如果使用在比較複雜的結構上,是真的會少很多時間!

不過還是要特別注記一下:

  1. 平行測試 不一定 會減少測試時間!
  2. 要小心不能共享資源,如果有,要小心使用~

跳過測試

如果遇到想要跳過的測試項目,我們可以用 t.Skip(“這裡放跳過原因”) 跳過,只驗證程式邏輯就好。

Go 提供 testing.Short() 方法,標記需要跳過測試的程式,而當我們輸入 go test -short 執行測試時,testing.Short() 會回傳 true ,然後一樣跑完測試會有輸出內容。在輸出內容中就可以看到被我們標記跳過的部分。

範例:

// 新增跳過標記的測試程式 calc_test.go
package calc
import "testing"

func TestAdd(t *testing.T) {
	tests := []struct {
		name string
		a, b int
		sum int
	}{
		{"test 1", 3, 2, 5},
		{"test 2", 5, 0, 5},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := Add(tt.a, tt.b)
			if testing.Short() {                        // 標記跳過測試
				t.Skip("跳過測試,因為跑 -short")
			}
			if got != tt.sum {                          // 所以這一段不會被執行
				t.Errorf("got %d, sum %d", got, tt.sum)
			}
		})
	}
}

輸出:
https://ithelp.ithome.com.tw/upload/images/20250921/20178223gqrsh8PsoP.png

從輸出的結果中會發現,測試程式並沒有執行「標記 testing.Short() 」的後面程式。且輸出的結果顯示的並不是失敗,而是寫上了 SKIP!然後順利跑完整個測試。

再來,還有一個比較特別的測試方式,就是 「使用註解的方式來表示正確的結果」 讓測試程式來比對!這個就稱為 Example 測試


Example 測試

測試的命名要使用 Example 開頭,例如:ExampleAdd 。而測試的方式就是看你輸入的結果是否等於註解的結果。是不是很特別~
範例:

// Example test
package calc
import "fmt"

func ExampleAdd() {
	fmt.Println(Add(2, 3))
	// Output: 5             // 這邊是放解答!
}

輸出:
https://ithelp.ithome.com.tw/upload/images/20250921/20178223gDZsTy85Or.png
👉 比對是否符合 // Output: 的結果。


Benchmark 測試

最後!除了驗證程式的邏輯是否正確之外,程式的效能也是很重要的~
而 Go 非常貼心,它也內建了效能檢測的方式,叫做 Benchmark 測試。一樣寫在 _test.go 的檔案裡面就可以了唷!

// 效能測試 **Benchmark**
package calc
import "testing"

func BenchmarkAdd(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Add(1, 2)
	}
}

執行時,輸入:

go test -bench=.

就會得到輸出:
https://ithelp.ithome.com.tw/upload/images/20250921/20178223XsNWe45ubK.png

Go 就會自己幫你計算運行這一個函式所用到的資源和結果,以下是輸出說明:

  • goos:作業系統。
  • goarch:CPU 架構。
  • pkg:測試是在哪邊運行的。
  • cpu:CPU 型號。
  • BenchmarkAdd-11 ,1000000000,0.3831 ns/op
    • BenchmarkAdd-11:測試函式名稱。
    • 11 :同時運行的 Goroutine 數量(由 Go 測試框架自動決定,和 CPU 核心數相關)。
    • 1000000000:執行次數。
    • 0.3831 ns/op:平均每次呼叫的時間,單位「奈秒」。
  • PASS,ok app/calc 0.968s
    • 最後的測試結果和執行所有測試花費的時間。

以上就是 Go 的單元測試介紹了~
內容雖然有一點多,但看完會發現其實 Go 的測試結構不算複雜,只要把所有的測試 function 放進 _test.go 的檔案裡面就可以了~


上一篇
Day6 - Go 的錯誤處理:error、panic 與 recover
下一篇
Day8 - 什麼是 Web API
系列文
Go,一起成為全端吧!—— 給前端工程師的 Golang 後端學習筆記12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言